/*
 * Copyright (C) 2014 Hortator
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package emergence.game.level.pathfinding;

import emergence.entities.Mob;
import emergence.game.level.Level;
import java.awt.Point;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 *
 * @author Nathan T
 */
public class Pathfinder {

    private final Mob mob;
    private Point start;
    private Point end;
    private int maxLength = -1;

    public Pathfinder(Mob mob) {
        this.mob = mob;
    }

    public void setStartPoint(Point p) {
        this.start = p;
    }

    public void setEndPoint(Point p) {
        this.end = p;
    }

    public void setMaxLength(int maxLength) {
        this.maxLength = maxLength;
    }
    
    public Path getShortestPath() {
        
        if (maxLength != -1) {
            int shortestLength = new PathNode(start, null).getH();
            if (shortestLength > maxLength) {
                return null;
            }
        }
        
        ArrayList<PathNode> openList = new ArrayList<>();
        ArrayList<PathNode> closedList = new ArrayList<>();
        closedList.add(new PathNode(start, null));
        boolean pathFound = false;
        
        while (!pathFound) {
            List<PathNode> adjacentNodes = viableNodesAdjacentTo(closedList.get(closedList.size() - 1));
            for (PathNode node : adjacentNodes) {
                if (!openList.contains(node) && !closedList.contains(node)) {
                    openList.add(node);
                }
            }
            PathNode nextNode = getNodeWithLowestF(openList);
            openList.remove(nextNode);
            closedList.add(nextNode);
            
            pathFound = nextNode.getPoint().equals(end);
            
            if (maxLength != -1) {
                if (nextNode.getG() > maxLength) {
                    return null;
                }
            }
        }
        
        ArrayList<Point> revPath = new ArrayList<>();
        PathNode curNode = closedList.get(closedList.size() - 1);
        boolean pathAssembled = (curNode == null);
        while (!pathAssembled) {
            revPath.add(curNode.getPoint());
            curNode = curNode.getParent();
            pathAssembled = (curNode == null);
        }
        
        ArrayList<Point> pathPoints = new ArrayList<>();
        for (int index = revPath.size() - 1; index >= 0; index--) {
            pathPoints.add(revPath.get(index));
        }
        
        return new Path(pathPoints);
    }

    private List<PathNode> viableNodesAdjacentTo(PathNode node) {
        int x = node.getPoint().x;
        int y = node.getPoint().y;
        List<PathNode> adjNodes = new ArrayList<>();

        if (mob.canTraverse(new Point(x, y + 1))) {
            adjNodes.add(new PathNode(new Point(x, y + 1), node));
        }
        if (mob.canTraverse(new Point(x, y - 1))) {
            adjNodes.add(new PathNode(new Point(x, y - 1), node));
        }
        if (mob.canTraverse(new Point(x + 1, y))) {
            adjNodes.add(new PathNode(new Point(x + 1, y), node));
        }
        if (mob.canTraverse(new Point(x - 1, y))) {
            adjNodes.add(new PathNode(new Point(x - 1, y), node));
        }

        return adjNodes;
    }

    private PathNode getNodeWithLowestF(ArrayList<PathNode> nodes) {
        if (nodes.isEmpty()) {
            return null;
        }
        PathNode lowestF = nodes.get(nodes.size() - 1);
        for (int index = nodes.size() - 2; index >= 0; index--) {
            if (nodes.get(index).getF() < lowestF.getF()) {
                lowestF = nodes.get(index);
            }
        }
        return lowestF;
    }
    
    private class PathNode {

        private final Point point;
        private PathNode parent;

        private int h;
        private int g;
        private int f;

        public PathNode(Point p, PathNode parent) {
            this.point = p;
            calcualteH();
            setParent(parent);
        }

        public Point getPoint() {
            return point;
        }

        public PathNode getParent() {
            return parent;
        }

        public int getH() {
            return h;
        }

        public int getG() {
            return g;
        }

        public int getF() {
            return f;
        }

        public final void setParent(PathNode newParent) {
            this.parent = newParent;
            calculateG();
            f = g + h;
        }

        private void calcualteH() {
            h = 0;
            h += Math.abs(this.point.x - end.x);
            h += Math.abs(this.point.y - end.y);
        }

        private void calculateG() {
            if (this.getParent() == null) {
                g = 0;
            }
            else {
                int count = 1;
                PathNode curNode = this.getParent();
                while (curNode != null) {
                    curNode = curNode.getParent();
                    count++;
                }
                g = count;
            }
        }

        @Override
        public boolean equals(Object other) {
            if (!(other instanceof PathNode)) {
                return false;
            }
            PathNode otherNode = (PathNode) other;
            return otherNode.getPoint().equals(this.getPoint());
        }

        @Override
        public int hashCode() {
            int hash = 5;
            hash = 79 * hash + Objects.hashCode(this.point);
            return hash;
        }
    }

}
